/*$Id:$*/

/**
 * Copyright vip_delete
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.googlecode.ideaplugin;

import net.sf.jadretro.Main;
import org.apache.maven.plugin.logging.Log;
import org.codehaus.plexus.util.FileUtils;
import org.codehaus.plexus.util.IOUtil;

import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import java.util.zip.ZipOutputStream;

/**
 * @author vip_delete
 * @since 23.04.2011
 */
class JadUtil
{
    /**
     * Decompile jarFile into sourceFile
     *
     * @param log        maven logger
     * @param jarFile    file with *.class files
     * @param sourceFile file with *.java files to be created
     * @param jadPath    path to jad (java decompiler) executable
     * @throws IOException exception
     */
    public static void decompileJar(Log log, File jarFile, File sourceFile, final String jadPath) throws Exception
    {
        log.info("Decompiling jar: " + jarFile.getAbsolutePath());

        String javaIOtmpDir = System.getProperty("java.io.tmpdir");
        String tmpName = "tmp_" + Long.toString(System.currentTimeMillis(), Character.MAX_RADIX);
        File tmpDir = new File(javaIOtmpDir, tmpName);
        File outDir = new File(javaIOtmpDir, tmpName + "_out");
        if (tmpDir.exists()) tmpDir.delete();
        if (outDir.exists()) outDir.delete();
        tmpDir.mkdir();
        outDir.mkdir();

        ZipInputStream in = new ZipInputStream(new FileInputStream(jarFile));

        ZipEntry zipEntry;

        while ((zipEntry = in.getNextEntry()) != null)
        {
            if (zipEntry.getName().endsWith(".class"))
            {
                File file = new File(tmpDir, zipEntry.getName());
                file.getParentFile().mkdirs();
                FileOutputStream out = new FileOutputStream(file);
                IOUtil.copy(in, out);
                out.close();
                Main.main(new String[]{file.getAbsolutePath()});
            }
        }

        in.close();

        String[] cmdArray = new String[]{jadPath, "-noctor", "-noinner", "-nonlb", "-lnc", "-space", "-sjava", "-r", "-d", outDir.getAbsolutePath().replace("\\", "/"), tmpDir.getAbsolutePath().replace("\\", "/") + "/**/*.class"};
        Process process = new ProcessBuilder(cmdArray).start();

        BackgroundReader backgroundReader = new BackgroundReader(process);
        new Thread(backgroundReader).start();
        int exitCode = process.waitFor();
        backgroundReader.stop();

        if (exitCode != 0)
        {
            log.warn("Cannot decompile jar: " + jarFile.getAbsolutePath());
            log.warn("Process ErrorStream");
            log.warn(new String(backgroundReader.getErrorStreamData()));
            log.warn("Process InputStream");
            log.warn(new String(backgroundReader.getInputStreamData()));
            return;
        }

        ByteArrayOutputStream byteArray = new ByteArrayOutputStream();
        ZipOutputStream out = new ZipOutputStream(byteArray);

        in = new ZipInputStream(new FileInputStream(jarFile));

        while ((zipEntry = in.getNextEntry()) != null)
        {
            String name = zipEntry.getName();
            if (zipEntry.isDirectory())
            {
                out.putNextEntry(new ZipEntry(name));
                out.closeEntry();
            } else if (name.endsWith(".class"))
            {
                String srcName = name.substring(0, name.length() - 5) + "java";
                File srcFile = new File(outDir, srcName);
                if (srcFile.exists())
                {
                    adjustLineNumbers(srcFile);
                    out.putNextEntry(new ZipEntry(srcName));
                    FileInputStream srcFileIn = new FileInputStream(srcFile);
                    IOUtil.copy(srcFileIn, out);
                    srcFileIn.close();
                    out.closeEntry();
                }
            } else
            {
                out.putNextEntry(new ZipEntry(name));
                IOUtil.copy(in, out);
                out.closeEntry();
            }
        }

        in.close();

        out.close();

        FileOutputStream sourceFileIn = new FileOutputStream(sourceFile);
        IOUtil.copy(byteArray.toByteArray(), sourceFileIn);
        sourceFileIn.close();

        FileUtils.deleteDirectory(tmpDir);
        FileUtils.deleteDirectory(outDir);
    }

    private static void adjustLineNumbers(File file) throws IOException
    {
        InputStream in = new FileInputStream(file);

        ByteArrayOutputStream lineOut = new ByteArrayOutputStream();

        List<byte[]> lineList = new ArrayList<byte[]>();

        lineList.add("// Decompiled by Jad. Copyright (C) 2001 Pavel Kouznetsov. http://www.varaneckas.com/jad".getBytes());
        lineList.add("// JAD helper is JadRetro. Copyright (C) 2007-2008 Ivan Maidanski <ivmai@mail.ru>. http://jadretro.sourceforge.net".getBytes());
        lineList.add("// Line number corrector is com.googlecode:maven-idea-plugin. Copyright (C) 2010-2011 Vasily Zhukov <vip.delete@gmail.com>. http://code.google.com/p/maven-idea-plugin".getBytes());

        List<Integer> lineNumberList = new ArrayList<Integer>();

        lineNumberList.add(null);
        lineNumberList.add(null);
        lineNumberList.add(null);

        int c = 0;

        boolean ignoreNextEmptyLine = false;

        while (c != -1)
        {
            while ((c = in.read()) != -1 && c != '\n') if (c != '\r') lineOut.write(c);

            byte[] line = lineOut.toByteArray();

            lineOut.reset();

            if (line.length > 2 && line[0] == '/' && line[1] == '/')
            {
            } else if (line.length > 8 && line[0] == '/' && line[1] == '*' && line[6] == '*' && line[7] == '/')
            {
                byte[] newLine = new byte[line.length - 8];
                System.arraycopy(line, 8, newLine, 0, newLine.length);
                lineList.add(newLine);
                lineNumberList.add(Integer.parseInt(new String(new byte[]{line[2], line[3], line[4], line[5]}).trim()));
                ignoreNextEmptyLine = false;
            } else if (line.length > 12 && line[0] == ' ' && line[1] == ' ' && line[2] == ' ' && line[3] == ' '
                    && line[4] == ' ' && line[5] == ' ' && line[6] == ' ' && line[7] == ' '
                    && line[8] == ' ' && line[9] == ' ' && line[10] == ' ' && line[11] == ' ')
            {
                byte[] newLine = new byte[line.length - 8];
                System.arraycopy(line, 8, newLine, 0, newLine.length);
                lineList.add(newLine);
                ignoreNextEmptyLine = newLine[4] == ' ' && newLine[5] == '*' && newLine[6] == '/';
                lineNumberList.add(null);
            } else if (line.length > 0 || !ignoreNextEmptyLine)
            {
                lineList.add(line);
                lineNumberList.add(null);
                ignoreNextEmptyLine = false;
            }
        }

        int len = lineNumberList.size();
        boolean prevWithoutNumber = false;

        for (int i = 0; i < len; i++)
        {
            Integer number = lineNumberList.get(i);

            if (number == null)
            {
                prevWithoutNumber = true;
            } else if (prevWithoutNumber)
            {
                prevWithoutNumber = false;
                int k = 1;
                while (i - k >= 0 && lineNumberList.get(i - k) == null && lineList.get(i - k).length != 0)
                {
                    lineNumberList.set(i - k, number - k);
                    k++;
                }
            }
        }

        // if no debug info, remove copyright about line number correction
        int j = 0;
        while (j < lineNumberList.size() && lineNumberList.get(j) == null) j++;
        if (j == lineNumberList.size())
        {
            lineList.remove(2);
            lineNumberList.remove(2);
            len--;
        }

        ByteArrayOutputStream out = new ByteArrayOutputStream();

        int index = 0;

        for (int i = 0; i < len; i++)
        {
            byte[] line = lineList.get(i);
            Integer number = lineNumberList.get(i);

            if (number == null)
            {
                if (line.length > 0)
                {
                    if (i > 0)
                    {
                        out.write('\r');
                        out.write('\n');
                    }
                    out.write(line);
                    index++;
                }
            } else
            {
                if (number < index)
                {
                    //System.out.println("Invalid line number: file=" + file.getAbsolutePath() + ", number=" + number + ", index=" + index);
                    number = index + 1;
                }

                if (number == index)
                {
                    int k = 0;
                    while (k < line.length && line[k] == ' ') k++;
                    out.write(' ');
                    out.write(line, k, line.length - k);
                } else
                {
                    for (int k = 0; k < number - index; k++)
                    {
                        out.write('\r');
                        out.write('\n');
                    }
                    out.write(line);
                    index = number;
                }
            }
        }

        out.close();

        FileOutputStream adjustedFileOut = new FileOutputStream(file);
        out.writeTo(adjustedFileOut);
        adjustedFileOut.close();
    }

    private static final class BackgroundReader implements Runnable
    {
        private ByteArrayOutputStream _err;
        private ByteArrayOutputStream _in;
        private Process _process;
        private boolean _working;

        private BackgroundReader(Process process)
        {
            _err = new ByteArrayOutputStream();
            _in = new ByteArrayOutputStream();
            _process = process;
            _working = true;
        }

        @Override
        public void run()
        {
            int c;
            InputStream errorStream = _process.getErrorStream();
            InputStream inputStream = _process.getInputStream();
            try
            {
                while (_working)
                {
                    if (errorStream.available() > 0)
                    {
                        c = errorStream.read();
                        if (c != -1)
                            _err.write(c);
                    }
                    if (inputStream.available() > 0)
                    {
                        c = inputStream.read();
                        if (c != -1)
                            _in.write(c);
                    }
                }
            } catch (IOException e)
            {
                e.printStackTrace();
                _process.destroy();
            }
        }

        public void stop()
        {
            _working = false;
        }

        public byte[] getErrorStreamData()
        {
            return _err.toByteArray();
        }

        public byte[] getInputStreamData()
        {
            return _in.toByteArray();
        }
    }
}
